Micron Document
baubs git

Node / mirrors / respira / commits / 101f46e

Commit 101f46e6272aa0c329c04e7840042f7782eb45f8


Parents : d213ab4
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-27T17:21:42+01:00

refactor: Extract color block calculation logic to utility module

Extract color block calculation from ProgressMonitor component to
colorBlockHelpers utility module for better testability and reusability.

Changes:
- Created colorBlockHelpers.ts with calculateColorBlocks() and findCurrentBlockIndex()
- Added comprehensive unit tests (11 test cases, all passing)
- Updated ProgressMonitor to use new utility functions
- Reduced component complexity by removing embedded business logic

Benefits:
- Logic can be tested in isolation
- Can be reused elsewhere if needed
- Cleaner component code
- Better separation of concerns

Fixes #44

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes

3 files changed, 311 insertions(+), 28 deletions(-)


Diff

diff --git a/src/components/ProgressMonitor/ProgressMonitor.tsx b/src/components/ProgressMonitor/ProgressMonitor.tsx
index b15e1dc..78ed296 100644
--- a/src/components/ProgressMonitor/ProgressMonitor.tsx
+++ b/src/components/ProgressMonitor/ProgressMonitor.tsx
@@ -12,6 +12,10 @@ import { usePatternStore } from "../../stores/usePatternStore";
import { ChartBarIcon } from "@heroicons/react/24/solid";
import { MachineStatus } from "../../types/machine";
import { calculatePatternTime } from "../../utils/timeCalculation";
+import {
+ calculateColorBlocks,
+ findCurrentBlockIndex,
+} from "../../utils/colorBlockHelpers";
import {
Card,
CardHeader,
@@ -23,7 +27,6 @@ import { ProgressStats } from "./ProgressStats";
import { ProgressSection } from "./ProgressSection";
import { ColorBlockList } from "./ColorBlockList";
import { ProgressActions } from "./ProgressActions";
-import type { ColorBlock } from "./types";
export function ProgressMonitor() {
// Machine store
@@ -69,36 +72,14 @@ export function ProgressMonitor() {
: 0;
// Calculate color block information from decoded penStitches
- const colorBlocks = useMemo(() => {
- if (!displayPattern || !displayPattern.penStitches) return [];
-
- const blocks: ColorBlock[] = [];
-
- // Use the pre-computed color blocks from decoded PEN data
- for (const penBlock of displayPattern.penStitches.colorBlocks) {
- const thread = displayPattern.threads[penBlock.colorIndex];
- blocks.push({
- colorIndex: penBlock.colorIndex,
- threadHex: thread?.hex || "#000000",
- threadCatalogNumber: thread?.catalogNumber ?? null,
- threadBrand: thread?.brand ?? null,
- threadDescription: thread?.description ?? null,
- threadChart: thread?.chart ?? null,
- startStitch: penBlock.startStitchIndex,
- endStitch: penBlock.endStitchIndex,
- stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex,
- });
- }
-
- return blocks;
- }, [displayPattern]);
+ const colorBlocks = useMemo(
+ () => calculateColorBlocks(displayPattern),
+ [displayPattern],
+ );
// Determine current color block based on current stitch
const currentStitch = sewingProgress?.currentStitch || 0;
- const currentBlockIndex = colorBlocks.findIndex(
- (block) =>
- currentStitch >= block.startStitch && currentStitch < block.endStitch,
- );
+ const currentBlockIndex = findCurrentBlockIndex(colorBlocks, currentStitch);
// Calculate time based on color blocks (matches Brother app calculation)
const { totalMinutes, elapsedMinutes } = useMemo(() => {

diff --git a/src/utils/colorBlockHelpers.test.ts b/src/utils/colorBlockHelpers.test.ts
new file mode 100644
index 0000000..8eb4af2
--- /dev/null
+++ b/src/utils/colorBlockHelpers.test.ts
@@ -0,0 +1,241 @@
+import { describe, it, expect } from "vitest";
+import {
+ calculateColorBlocks,
+ findCurrentBlockIndex,
+} from "./colorBlockHelpers";
+import type { PesPatternData } from "../formats/import/client";
+
+describe("colorBlockHelpers", () => {
+ describe("calculateColorBlocks", () => {
+ it("should return empty array when displayPattern is null", () => {
+ const result = calculateColorBlocks(null);
+ expect(result).toEqual([]);
+ });
+
+ it("should return empty array when penStitches is undefined", () => {
+ const pattern = {
+ penStitches: undefined,
+ } as unknown as PesPatternData;
+
+ const result = calculateColorBlocks(pattern);
+ expect(result).toEqual([]);
+ });
+
+ it("should calculate color blocks from PEN data", () => {
+ const pattern: Partial<PesPatternData> = {
+ threads: [
+ {
+ color: 1,
+ hex: "#FF0000",
+ brand: "Brother",
+ catalogNumber: "001",
+ description: "Red",
+ chart: "A",
+ },
+ {
+ color: 2,
+ hex: "#00FF00",
+ brand: "Brother",
+ catalogNumber: "002",
+ description: "Green",
+ chart: "B",
+ },
+ ],
+ penStitches: {
+ colorBlocks: [
+ {
+ startStitchIndex: 0,
+ endStitchIndex: 100,
+ colorIndex: 0,
+ startStitch: 0,
+ endStitch: 100,
+ },
+ {
+ startStitchIndex: 100,
+ endStitchIndex: 250,
+ colorIndex: 1,
+ startStitch: 100,
+ endStitch: 250,
+ },
+ ],
+ stitches: [],
+ bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 },
+ },
+ };
+
+ const result = calculateColorBlocks(pattern as PesPatternData);
+
+ expect(result).toHaveLength(2);
+ expect(result[0]).toEqual({
+ colorIndex: 0,
+ threadHex: "#FF0000",
+ threadCatalogNumber: "001",
+ threadBrand: "Brother",
+ threadDescription: "Red",
+ threadChart: "A",
+ startStitch: 0,
+ endStitch: 100,
+ stitchCount: 100,
+ });
+ expect(result[1]).toEqual({
+ colorIndex: 1,
+ threadHex: "#00FF00",
+ threadCatalogNumber: "002",
+ threadBrand: "Brother",
+ threadDescription: "Green",
+ threadChart: "B",
+ startStitch: 100,
+ endStitch: 250,
+ stitchCount: 150,
+ });
+ });
+
+ it("should use fallback values when thread data is missing", () => {
+ const pattern: Partial<PesPatternData> = {
+ threads: [],
+ penStitches: {
+ colorBlocks: [
+ {
+ startStitchIndex: 0,
+ endStitchIndex: 50,
+ colorIndex: 0,
+ startStitch: 0,
+ endStitch: 50,
+ },
+ ],
+ stitches: [],
+ bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 },
+ },
+ };
+
+ const result = calculateColorBlocks(pattern as PesPatternData);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual({
+ colorIndex: 0,
+ threadHex: "#000000", // Fallback for missing thread
+ threadCatalogNumber: null,
+ threadBrand: null,
+ threadDescription: null,
+ threadChart: null,
+ startStitch: 0,
+ endStitch: 50,
+ stitchCount: 50,
+ });
+ });
+
+ it("should handle null thread metadata fields", () => {
+ const pattern: Partial<PesPatternData> = {
+ threads: [
+ {
+ color: 1,
+ hex: "#0000FF",
+ brand: null,
+ catalogNumber: null,
+ description: null,
+ chart: null,
+ },
+ ],
+ penStitches: {
+ colorBlocks: [
+ {
+ startStitchIndex: 0,
+ endStitchIndex: 30,
+ colorIndex: 0,
+ startStitch: 0,
+ endStitch: 30,
+ },
+ ],
+ stitches: [],
+ bounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 },
+ },
+ };
+
+ const result = calculateColorBlocks(pattern as PesPatternData);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual({
+ colorIndex: 0,
+ threadHex: "#0000FF",
+ threadCatalogNumber: null,
+ threadBrand: null,
+ threadDescription: null,
+ threadChart: null,
+ startStitch: 0,
+ endStitch: 30,
+ stitchCount: 30,
+ });
+ });
+ });
+
+ describe("findCurrentBlockIndex", () => {
+ const colorBlocks = [
+ {
+ colorIndex: 0,
+ threadHex: "#FF0000",
+ threadCatalogNumber: "001",
+ threadBrand: "Brother",
+ threadDescription: "Red",
+ threadChart: "A",
+ startStitch: 0,
+ endStitch: 100,
+ stitchCount: 100,
+ },
+ {
+ colorIndex: 1,
+ threadHex: "#00FF00",
+ threadCatalogNumber: "002",
+ threadBrand: "Brother",
+ threadDescription: "Green",
+ threadChart: "B",
+ startStitch: 100,
+ endStitch: 250,
+ stitchCount: 150,
+ },
+ {
+ colorIndex: 2,
+ threadHex: "#0000FF",
+ threadCatalogNumber: "003",
+ threadBrand: "Brother",
+ threadDescription: "Blue",
+ threadChart: "C",
+ startStitch: 250,
+ endStitch: 400,
+ stitchCount: 150,
+ },
+ ];
+
+ it("should find block containing stitch at start boundary", () => {
+ expect(findCurrentBlockIndex(colorBlocks, 0)).toBe(0);
+ expect(findCurrentBlockIndex(colorBlocks, 100)).toBe(1);
+ expect(findCurrentBlockIndex(colorBlocks, 250)).toBe(2);
+ });
+
+ it("should find block containing stitch in middle", () => {
+ expect(findCurrentBlockIndex(colorBlocks, 50)).toBe(0);
+ expect(findCurrentBlockIndex(colorBlocks, 150)).toBe(1);
+ expect(findCurrentBlockIndex(colorBlocks, 300)).toBe(2);
+ });
+
+ it("should return -1 for stitch before first block", () => {
+ expect(findCurrentBlockIndex(colorBlocks, -1)).toBe(-1);
+ });
+
+ it("should return -1 for stitch at or after last block end", () => {
+ expect(findCurrentBlockIndex(colorBlocks, 400)).toBe(-1);
+ expect(findCurrentBlockIndex(colorBlocks, 500)).toBe(-1);
+ });
+
+ it("should return -1 for empty color blocks array", () => {
+ expect(findCurrentBlockIndex([], 50)).toBe(-1);
+ });
+
+ it("should find block with single color block", () => {
+ const singleBlock = [colorBlocks[0]];
+ expect(findCurrentBlockIndex(singleBlock, 0)).toBe(0);
+ expect(findCurrentBlockIndex(singleBlock, 50)).toBe(0);
+ expect(findCurrentBlockIndex(singleBlock, 99)).toBe(0);
+ expect(findCurrentBlockIndex(singleBlock, 100)).toBe(-1);
+ });
+ });
+});

diff --git a/src/utils/colorBlockHelpers.ts b/src/utils/colorBlockHelpers.ts
new file mode 100644
index 0000000..20d2b22
--- /dev/null
+++ b/src/utils/colorBlockHelpers.ts
@@ -0,0 +1,61 @@
+/**
+ * Color Block Helpers
+ *
+ * Utility functions for calculating color block information from pattern data.
+ * Extracted from ProgressMonitor component for better testability and reusability.
+ */
+
+import type { PesPatternData } from "../formats/import/client";
+import type { ColorBlock } from "../components/ProgressMonitor/types";
+
+/**
+ * Calculate color blocks from decoded PEN pattern data
+ *
+ * Transforms PEN color blocks into enriched ColorBlock objects with thread metadata.
+ * Returns an empty array if pattern or penStitches data is unavailable.
+ *
+ * @param displayPattern - The PES pattern data containing penStitches and threads
+ * @returns Array of ColorBlock objects with thread information and stitch counts
+ */
+export function calculateColorBlocks(
+ displayPattern: PesPatternData | null,
+): ColorBlock[] {
+ if (!displayPattern || !displayPattern.penStitches) return [];
+
+ const blocks: ColorBlock[] = [];
+
+ // Use the pre-computed color blocks from decoded PEN data
+ for (const penBlock of displayPattern.penStitches.colorBlocks) {
+ const thread = displayPattern.threads[penBlock.colorIndex];
+ blocks.push({
+ colorIndex: penBlock.colorIndex,
+ threadHex: thread?.hex || "#000000",
+ threadCatalogNumber: thread?.catalogNumber ?? null,
+ threadBrand: thread?.brand ?? null,
+ threadDescription: thread?.description ?? null,
+ threadChart: thread?.chart ?? null,
+ startStitch: penBlock.startStitchIndex,
+ endStitch: penBlock.endStitchIndex,
+ stitchCount: penBlock.endStitchIndex - penBlock.startStitchIndex,
+ });
+ }
+
+ return blocks;
+}
+
+/**
+ * Find the index of the color block containing a specific stitch
+ *
+ * @param colorBlocks - Array of color blocks to search
+ * @param currentStitch - The stitch index to find
+ * @returns The index of the containing block, or -1 if not found
+ */
+export function findCurrentBlockIndex(
+ colorBlocks: ColorBlock[],
+ currentStitch: number,
+): number {
+ return colorBlocks.findIndex(
+ (block) =>
+ currentStitch >= block.startStitch && currentStitch < block.endStitch,
+ );
+}

Served by rngit 1.3.3 - Generated in 0.06s